#!/usr/bin/env python3
"""
OPC UA配置和连接诊断脚本
用于诊断节点获取失败的具体原因
"""

import json
import sys
import os
import asyncio
import logging

# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(__file__))

try:
    from asyncua import Client, ua
    OPCUA_AVAILABLE = True
except ImportError:
    OPCUA_AVAILABLE = False

def analyze_device_config():
    """分析devices.json中的OPC UA配置"""
    print("🔍 分析devices.json中的OPC UA配置...")
    
    try:
        with open('config/devices.json', 'r', encoding='utf-8') as f:
            devices_config = json.load(f)
        
        opcua_devices = [d for d in devices_config.get('devices', []) if d.get('type') == 'opcua']
        
        print(f"📊 找到 {len(opcua_devices)} 个OPC UA设备")
        
        for device in opcua_devices:
            print(f"\n📄 设备: {device.get('name')}")
            print(f"  ├─ 描述: {device.get('description')}")
            print(f"  ├─ 启用状态: {device.get('enabled')}")
            print(f"  ├─ 轮询间隔: {device.get('polling_interval')}ms")
            
            config = device.get('config', {})
            print(f"  ├─ 端点: {config.get('endpoint')}")
            print(f"  ├─ 安全策略: {config.get('security_policy')}")
            print(f"  ├─ 用户名: {config.get('username', '未设置')}")
            
            points = device.get('points', [])
            print(f"  └─ 数据点 ({len(points)}个):")
            
            for point in points:
                print(f"      ├─ {point.get('name')}: {point.get('description')}")
                print(f"      │   ├─ 节点ID: {point.get('node_id')}")
                print(f"      │   ├─ 数据类型: {point.get('data_type')}")
                print(f"      │   ├─ 访问权限: {point.get('access')}")
                print(f"      │   └─ 单位: {point.get('unit', '无')}")
                
                # 验证节点ID格式
                node_id = point.get('node_id')
                if node_id:
                    if not node_id.startswith('ns='):
                        print(f"      ⚠️  警告: 节点ID格式可能有误: {node_id}")
                    else:
                        print(f"      ✅ 节点ID格式正确")
                else:
                    print(f"      ❌ 错误: 缺少节点ID")
        
        return opcua_devices
        
    except FileNotFoundError:
        print("❌ 未找到config/devices.json文件")
        return []
    except Exception as e:
        print(f"❌ 读取配置文件失败: {e}")
        return []

async def test_opcua_connection(endpoint):
    """测试OPC UA连接"""
    print(f"\n🔌 测试OPC UA连接: {endpoint}")
    
    if not OPCUA_AVAILABLE:
        print("❌ asyncua库未安装，请运行: pip install asyncua")
        return None
    
    client = Client(url=endpoint)
    
    try:
        print("  ├─ 正在连接...")
        await client.connect()
        print("  ✅ 连接成功")
        
        # 获取根节点信息
        objects = client.get_objects_node()
        print(f"  ├─ Objects节点: {objects}")
        
        # 获取命名空间
        namespaces = await client.get_namespace_array()
        print(f"  ├─ 命名空间数量: {len(namespaces)}")
        for i, ns in enumerate(namespaces):
            print(f"  │   └─ ns={i}: {ns}")
        
        # 浏览根节点的子节点
        print("  ├─ 浏览Objects下的子节点:")
        children = await objects.get_children()
        for child in children[:10]:  # 只显示前10个
            try:
                display_name = await child.read_display_name()
                node_id = child.nodeid
                print(f"  │   ├─ {display_name.Text} (ID: {node_id})")
            except Exception as e:
                print(f"  │   ├─ 无法读取节点信息: {e}")
        
        if len(children) > 10:
            print(f"  │   └─ ... 还有 {len(children) - 10} 个节点")
            
        return client
        
    except Exception as e:
        print(f"  ❌ 连接失败: {e}")
        return None

async def test_node_access(client, node_id):
    """测试节点访问"""
    print(f"\n🎯 测试节点访问: {node_id}")
    
    try:
        # 获取节点
        node = client.get_node(node_id)
        print(f"  ├─ 节点对象: {node}")
        
        # 读取节点属性
        try:
            display_name = await node.read_display_name()
            print(f"  ├─ 显示名称: {display_name.Text}")
        except Exception as e:
            print(f"  ├─ 无法读取显示名称: {e}")
        
        try:
            browse_name = await node.read_browse_name()
            print(f"  ├─ 浏览名称: {browse_name}")
        except Exception as e:
            print(f"  ├─ 无法读取浏览名称: {e}")
        
        try:
            node_class = await node.read_node_class()
            print(f"  ├─ 节点类: {node_class}")
        except Exception as e:
            print(f"  ├─ 无法读取节点类: {e}")
        
        try:
            data_type = await node.read_data_type()
            print(f"  ├─ 数据类型: {data_type}")
        except Exception as e:
            print(f"  ├─ 无法读取数据类型: {e}")
        
        # 尝试读取值
        try:
            value = await node.read_value()
            print(f"  ✅ 节点值: {value}")
            return True
        except Exception as e:
            print(f"  ❌ 读取值失败: {e}")
            return False
            
    except Exception as e:
        print(f"  ❌ 节点访问失败: {e}")
        return False

async def browse_namespace(client, namespace_index=2):
    """浏览指定命名空间的节点"""
    print(f"\n🔍 浏览命名空间 ns={namespace_index} 的节点...")
    
    try:
        # 从根节点开始浏览
        objects = client.get_objects_node()
        await _browse_children(objects, depth=0, max_depth=3, target_ns=namespace_index)
        
    except Exception as e:
        print(f"❌ 浏览命名空间失败: {e}")

async def _browse_children(node, depth=0, max_depth=2, target_ns=None):
    """递归浏览子节点"""
    if depth > max_depth:
        return
    
    try:
        children = await node.get_children()
        indent = "  " * depth
        
        for child in children:
            try:
                display_name = await child.read_display_name()
                node_id = child.nodeid
                
                # 如果指定了目标命名空间，只显示该命名空间的节点
                if target_ns is None or node_id.NamespaceIndex == target_ns:
                    print(f"{indent}├─ {display_name.Text} (ID: {node_id})")
                    
                    # 如果深度允许，继续浏览子节点
                    if depth < max_depth:
                        await _browse_children(child, depth + 1, max_depth, target_ns)
                        
            except Exception as e:
                print(f"{indent}├─ 无法读取节点: {e}")
                
    except Exception as e:
        print(f"浏览子节点失败: {e}")

async def suggest_correct_node_id(client, target_name="H1SP1_ATemp"):
    """建议正确的节点ID"""
    print(f"\n🎯 搜索可能的节点: {target_name}")
    
    try:
        # 搜索包含目标名称的节点
        await _search_nodes_by_name(client, target_name)
        
    except Exception as e:
        print(f"❌ 搜索节点失败: {e}")

async def _search_nodes_by_name(client, target_name, node=None, depth=0, max_depth=4):
    """按名称搜索节点"""
    if depth > max_depth:
        return
        
    if node is None:
        node = client.get_objects_node()
    
    try:
        children = await node.get_children()
        
        for child in children:
            try:
                display_name = await child.read_display_name()
                browse_name = await child.read_browse_name()
                node_id = child.nodeid
                
                # 检查名称匹配
                if (target_name.lower() in display_name.Text.lower() or 
                    target_name.lower() in browse_name.Name.lower()):
                    print(f"  ✅ 找到匹配节点:")
                    print(f"      ├─ 显示名称: {display_name.Text}")
                    print(f"      ├─ 浏览名称: {browse_name.Name}")
                    print(f"      └─ 节点ID: {node_id}")
                
                # 递归搜索子节点
                if depth < max_depth:
                    await _search_nodes_by_name(client, target_name, child, depth + 1, max_depth)
                    
            except Exception:
                # 忽略无法访问的节点
                pass
                
    except Exception:
        # 忽略无法浏览的节点
        pass

async def main():
    """主函数"""
    print("🚀 OPC UA配置和连接诊断工具")
    print("=" * 50)
    
    # 1. 分析配置文件
    devices = analyze_device_config()
    
    if not devices:
        print("❌ 没有找到OPC UA设备配置")
        return
    
    # 找到1#混流泵设备
    target_device = None
    for device in devices:
        if "混流泵" in device.get('name', ''):
            target_device = device
            break
    
    if not target_device:
        print("❌ 未找到1#混流泵设备配置")
        return
    
    endpoint = target_device.get('config', {}).get('endpoint')
    if not endpoint:
        print("❌ 设备配置中缺少端点URL")
        return
    
    # 2. 测试连接
    client = await test_opcua_connection(endpoint)
    
    if not client:
        print("❌ 无法连接到OPC UA服务器，请检查:")
        print("  1. 服务器是否正在运行")
        print("  2. 端点地址是否正确")
        print("  3. 网络连接是否正常")
        return
    
    try:
        # 3. 测试当前配置的节点
        for point in target_device.get('points', []):
            node_id = point.get('node_id')
            if node_id:
                await test_node_access(client, node_id)
        
        # 4. 浏览命名空间
        await browse_namespace(client, namespace_index=2)
        
        # 5. 搜索正确的节点
        for point in target_device.get('points', []):
            point_name = point.get('name')
            if point_name:
                await suggest_correct_node_id(client, point_name)
        
    finally:
        await client.disconnect()
        print("\n🔌 已断开OPC UA连接")

if __name__ == "__main__":
    asyncio.run(main()) 